Use IdentityArrayMap instead of HashMap inside DerivedState Updates DerivedState implementation to use IdentityArrayMap for faster iteration. This change exposes internal `keys` array of dependency map with potentially empty elements, which we have to skip over when reading. Changes `DerivedState.currentValue` to only return the value without forcing record of transitive dependency reads. Change-Id: Ica516bba042781f03e21fda7628b9788f9e018bd 
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt index 1b652e8..65a96b1 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt 
@@ -710,7 +710,9 @@  // Record derived state dependency mapping  if (value is DerivedState<*>) {  derivedStates.removeScope(value) - value.dependencies.forEach { dependency -> + for (dependency in value.dependencies) { + // skip over empty objects from dependency array + if (dependency == null) break  derivedStates.add(dependency, value)  }  } 
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt index ee172c5..4563c63 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt 
@@ -18,6 +18,7 @@  @file:JvmMultifileClass  package androidx.compose.runtime   +import androidx.compose.runtime.collection.IdentityArrayMap  import androidx.compose.runtime.external.kotlinx.collections.immutable.PersistentList  import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentListOf  import androidx.compose.runtime.snapshots.Snapshot @@ -50,7 +51,7 @@  * The [dependencies] list can be used to determine when a [StateObject] appears in the apply  * observer set, if the state could affect value of this derived state.  */ - val dependencies: Set<StateObject> + val dependencies: Array<Any?>    /**  * Mutation policy that controls how changes are handled after state dependencies update. @@ -77,7 +78,7 @@  val Unset = Any()  }   - var dependencies: HashMap<StateObject, Int>? = null + var dependencies: IdentityArrayMap<StateObject, Int>? = null  var result: Any? = Unset  var resultHash: Int = 0   @@ -99,9 +100,9 @@  val dependencies = sync { dependencies }  if (dependencies != null) {  notifyObservers(derivedState) { - for ((stateObject, readLevel) in dependencies.entries) { + dependencies.forEach { stateObject, readLevel ->  if (readLevel != 1) { - continue + return@forEach  }    if (stateObject is DerivedSnapshotState<*>) { @@ -140,9 +141,9 @@  // for correct invalidation later  if (forceDependencyReads) {  notifyObservers(this) { - val dependencies = readable.dependencies ?: emptyMap() + val dependencies = readable.dependencies  val invalidationNestedLevel = calculationBlockNestedLevel.get() ?: 0 - for ((dependency, nestedLevel) in dependencies) { + dependencies?.forEach { dependency, nestedLevel ->  calculationBlockNestedLevel.set(nestedLevel + invalidationNestedLevel)  snapshot.readObserver?.invoke(dependency)  } @@ -153,7 +154,7 @@  }  val nestedCalculationLevel = calculationBlockNestedLevel.get() ?: 0   - val newDependencies = HashMap<StateObject, Int>() + val newDependencies = IdentityArrayMap<StateObject, Int>()  val result = notifyObservers(this) {  calculationBlockNestedLevel.set(nestedCalculationLevel + 1)   @@ -218,18 +219,23 @@  // value is used instead which doesn't notify. This allow the read observer to read the  // value and only update the cache once.  Snapshot.current.readObserver?.invoke(this) - return currentValue + return first.withCurrent { + @Suppress("UNCHECKED_CAST") + currentRecord(it, Snapshot.current, true, calculation).result as T + }  }    override val currentValue: T  get() = first.withCurrent {  @Suppress("UNCHECKED_CAST") - currentRecord(it, Snapshot.current, true, calculation).result as T + currentRecord(it, Snapshot.current, false, calculation).result as T  }   - override val dependencies: Set<StateObject> + override val dependencies: Array<Any?>  get() = first.withCurrent { - currentRecord(it, Snapshot.current, false, calculation).dependencies?.keys ?: emptySet() + val record = currentRecord(it, Snapshot.current, false, calculation) + @Suppress("UNCHECKED_CAST") + record.dependencies?.keys ?: emptyArray()  }    override fun toString(): String = first.withCurrent { 
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt index 55240ef..3a7cf06 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt 
@@ -279,6 +279,8 @@  dependencyToDerivedStates.removeScope(value)  val dependencies = value.dependencies  for (dependency in dependencies) { + // skip over dependency array + if (dependency == null) break  dependencyToDerivedStates.add(dependency, value)  }  derivedStateToValue[value] = value.currentValue